/**
 * 
 */
package jpb.test.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

import org.apache.commons.io.IOUtils;

/**
 * Utility to create JAR files out of directories.
 * 
 * @author Dariusz Kordonski
 */
public final class JarBuilder {

    private final File sourceDir;
    private Manifest manifest;
    
    public JarBuilder(String sourceDirPath) {
        File dir = new File(sourceDirPath);
        if (!dir.isDirectory()) {
            throw new IllegalArgumentException("Path [" + sourceDirPath + "] does not point to an"
                    + " existing directory");
        }
        this.sourceDir = dir;
    }
    
    public JarBuilder manifest(Manifest manifest) {
        if (manifest == null) {
            throw new NullPointerException("manifest");
        }
        this.manifest = manifest; return this;
    }

    public JarBuilder clearManifest() {
        this.manifest = null; return this;
    }

    public Manifest manifest() {
        return this.manifest;
    }
    
    public boolean hasManifest() {
        return this.manifest != null;
    }
    
    public JarBuilder mainAttribute(Attributes.Name attrName, Object value) {
        this.manifest.getMainAttributes().put(attrName, value);
        return this;
    }
    
    public JarBuilder attribute(String entryName, String attrName, Object value) {
        Attributes attrs = this.manifest.getAttributes(entryName);
        if (attrs == null) {
            attrs = new Attributes();
            this.manifest.getEntries().put(entryName, attrs);
        }
        attrs.put(attrName, value);
        return this;
    }
    
    
    /**
     * 
     * 
     * @param outputPath
     * @return
     * @throws IOException
     */
    public File createJar(String outputPath) throws IOException {
        File output = jarFile(outputPath);
        JarOutputStream jout = openJarStream(output);
        try {
            jarFiles(filesToJar(), jout);
            return output;
        } finally {
            IOUtils.closeQuietly(jout);
        }
    }
    
    private JarOutputStream openJarStream(File output) throws IOException {
        verifyManifestVersion();
        FileOutputStream fout = new FileOutputStream(output);
        if (manifest != null) {
            return new JarOutputStream(fout, manifest);
        } else {
            return new JarOutputStream(fout);
        }
    }

    /**
     * If Manifest does not have {@link Attributes.Name#MANIFEST_VERSION}
     * or {@link Attributes.Name#SIGNATURE_VERSION} attribute defined,
     * the Java API will silently skip writing any contents into
     * the manifest file, which may provoke confusing behavior.
     * This method averts it by enforcing that one of those attributes
     * be set in the target Manifest, if it is defined at all.
     */
    private void verifyManifestVersion() {
        if (manifest != null) {
            if (hasNoVersionAttributes(manifest)) {
                throw new IllegalStateException("Manifest file defined but has no version attributes. According to"
                        + " the JAR specification at least one of the following attributes must be defined in the"
                        + " manifest file: '" + Attributes.Name.MANIFEST_VERSION + "' or '"
                        + Attributes.Name.SIGNATURE_VERSION + "'");
            }
        }
    }

    private boolean hasNoVersionAttributes(Manifest manifest) {
        return !manifest.getMainAttributes().containsKey(Attributes.Name.MANIFEST_VERSION)
            && !manifest.getMainAttributes().containsKey(Attributes.Name.SIGNATURE_VERSION);
    }

    private File jarFile(String outputPath) {
        File output = new File(outputPath);
        if (output.exists()) {
            throw new IllegalArgumentException("Path [" + outputPath + "] points to an existing file"
                    + " or directory");
        }
        return output;
    }

    private void jarFiles(List<File> filesToJar, JarOutputStream jout) throws IOException {
        for (File toJar : filesToJar) {
            jout.putNextEntry(new JarEntry(relativePath(toJar)));
            copyToJar(toJar, jout);
            jout.closeEntry();
        }
    }
    
    private void copyToJar(File toJar, JarOutputStream jout) throws IOException {
        InputStream is = null;
        try {
            is = new FileInputStream(toJar);
            IOUtils.copy(is, jout);
        } finally {
            IOUtils.closeQuietly(is);
        }    
    }

    private List<File> filesToJar() {
        return filesToJar(sourceDir);
    }
    
    private List<File> filesToJar(File output) {
        final List<File> result = new ArrayList<File>();
        for (File each : output.listFiles()) {
            if (each.isDirectory()) {
                result.addAll(filesToJar(each));
            } else {
                result.add(each);
            }
        }
        return result;
    }
    
    private String relativePath(File toJar) {
        final Deque<File> fullPath = collectPath(toJar);
        StringBuilder sb = new StringBuilder();
        File next = null;
        while ((next = fullPath.pollFirst()) != null) {
            sb.append(next.getName()).append(next != toJar ? File.separator : "");
        }
        return sb.toString();        
    }

    private Deque<File> collectPath(File toJar) {
        Deque<File> fullPath = new LinkedList<File>();
        File parent = toJar;
        while (!parent.equals(sourceDir)) {
            fullPath.addFirst(parent);
            parent = parent.getParentFile();
        }
        return fullPath;
    }
    
}
